﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using FarseerGames.FarseerPhysics;
using FarseerGames.FarseerPhysics.Dynamics.Joints;
using FarseerGames.FarseerPhysics.Mathematics;
using FreeGoo.GameEngine.Interfaces;
using FreeGoo.GameEngine.Renderables;
using FreeGoo.GameEngine.Renderables.Links;
using FreeGoo.GameEngine.Renderables.Particles;
using FreeGoo.GameEngine.Tasks;

namespace FreeGoo.GameEngine
{
    public class Engine
    {
        public const float PhysicsTimeStep = 1 / 100f;//0.01f;
        public static readonly CollisionCategory UnlinkedParticleCollisionCategory = CollisionCategory.Cat1;
        public static readonly CollisionCategory LinkedParticleCollisionCategory = CollisionCategory.Cat2;
        public static readonly CollisionCategory NonParticleCollisionCategory = CollisionCategory.Cat3;
        public static readonly CollisionCategory StaticCollisionCategory = CollisionCategory.Cat31;

        private readonly Stopwatch _stopwatch = new Stopwatch();
        private ILevel _currentLevel = null;

        public Engine(Vector2 size, IRendererFactory rendererFactory)
        {
            PhysicsSimulator = new PhysicsSimulator(MixedFactory.Gravity);
            Renderables = new List<Renderable>();
            BodyRenderables = new List<BodyRenderable>();
            Particles = new List<Particle>();
            Scripts = new List<IEngineScript>();
            Links = new List<Link>();
            Goals = new List<Goal>();
            Linkables = new List<ILinkable>();
            Size = size;

            RendererFactory = rendererFactory;
            RendererFactory.Engine = this;
            UserInputController = new UserInputController(this);
            TimeStep = PhysicsTimeStep;
            KillZone = new Vector2(300, 120);
            Random = new Random();
        }

        public event EventHandler<EventArgs> LevelLoaded;

        public IList<Particle> Particles { get; private set; }
        public IList<Renderable> Renderables { get; private set; }
        public IList<BodyRenderable> BodyRenderables { get; private set; }
        public IList<ILinkable> Linkables { get; private set; }
        public IList<Link> Links { get; private set; }
        public IList<Goal> Goals { get; private set; }
        public IList<IEngineScript> Scripts { get; private set; }

        public Random Random { get; private set; }

        public ILevel CurrentLevel
        {
            get
            {
                return _currentLevel;
            }

            set
            {
                _currentLevel = value;
                LoadLevel();
            }
        }

        public int TotalRescuedCount
        {
            get
            {
                return Goals.Sum(goal => goal.RescuedCount);
            }
        }

        public PhysicsSimulator PhysicsSimulator { get; private set; }
        public IRendererFactory RendererFactory { get; private set; }
        public UserInputController UserInputController { get; private set; }
        public float TimeStep { get; set; }
        public float CurrentTime { get; protected set; }
        public Vector2 KillZone { get; set; }
        public Task Task { get; set; }

        public double PhysicsTime { get; private set; }
        public int PhysicsFrameCount { get; private set; }
        public double RenderTime { get; private set; }
        public int RenderFrameCount { get; private set; }
        public int PhysicsFrameNumber { get; private set; }

        public Vector2 Size { get; set; }

        public void ResetStatistics()
        {
            PhysicsTime = 0;
            PhysicsFrameCount = 0;
            RenderTime = 0;
            RenderFrameCount = 0;
        }

        public void Add(Renderable renderable)
        {
            renderable.AddToEngine(this);
        }

        public bool LoadLevel()
        {
            ClearLevel();
            if (CurrentLevel != null)
            {
                Size = CurrentLevel.Size;
                RendererFactory.DoBeforeCopyLevelToEngine();
                CurrentLevel.CopyToEngine(this);
                RendererFactory.DoAfterCopyLevelToEngine();
                DoLevelLoaded();
                return true;
            }
            else
            {
                return false;
            }
        }

        public void ProgressUtil(float targetTime)
        {
            int frames = 0;
            _stopwatch.Reset();
            _stopwatch.Start();

            if (targetTime - CurrentTime > TimeStep * 10)
            {
                // If we run too far behind, start moving slower
                CurrentTime = targetTime - TimeStep;
            }

            while (CurrentTime + TimeStep < targetTime)
            {
                DoUpdate();
                CurrentTime = CurrentTime + TimeStep;
                frames++;
            }

            PhysicsFrameCount += frames;
            PhysicsTime += _stopwatch.Elapsed.TotalMilliseconds;

            if (frames > 0)
            {
                UpdateRenderablesGraphics();
            }
        }

        public Renderable GetRenderableById(string identifier)
        {
            Renderable result = Renderables.Where(renderable => identifier.Equals(renderable.Identifier)).FirstOrDefault();

            if (result == null)
            {
                throw new FreeGooException("Unable to locate item with identifier {0}!", identifier);
            }

            return result;
        }

        public void AddScript(IEngineScript script)
        {
            Scripts.Add(script);
            script.Prepare(this);
        }

        // Todo: Use GetParticlesNearPosition!
        public Particle GetParticleForPosition(Vector2 vector2, bool onlyAwake)
        {
            float bestDistanceSquared = float.MaxValue;
            Particle bestParticle = null;
            foreach (Particle particle in Particles)
            {
                if (onlyAwake && particle.IsSleeping)
                {
                    continue;
                }

                float distanceSquared;
                if (particle.GetPointIsInParticle(vector2, out distanceSquared))
                {
                    if (distanceSquared < bestDistanceSquared)
                    {
                        bestDistanceSquared = distanceSquared;
                        bestParticle = particle;
                    }
                }
            }

            return bestParticle;
        }

        // Todo: Use spatial partitioning of physics engine!
        public List<Particle> GetParticlesNearPosition(Vector2 position, float selectionRadius)
        {
            List<Particle> result = new List<Particle>();
            float selectionRadiusSquared = selectionRadius * selectionRadius;

            foreach (Particle particle in Particles)
            {
                float distanceSquared = Vector2.DistanceSquared(particle.Body.Position, position);
                if (distanceSquared < selectionRadiusSquared)
                {
                    particle.LastTestDistance = (float)Math.Sqrt(distanceSquared);
                    result.Add(particle);
                }
            }

            return result.OrderBy(particle => particle.LastTestDistance).ToList();
        }

        public List<ILinkable> GetLinkablesNearPosition(Vector2 position, float selectionRadius)
        {
            List<ILinkable> result = new List<ILinkable>();
            float selectionRadiusSquared = selectionRadius * selectionRadius;

            foreach (ILinkable linkable in Linkables)
            {
                float distanceSquared = Vector2.DistanceSquared(linkable.Position, position);
                if (distanceSquared < selectionRadiusSquared)
                {
                    linkable.LastTestDistance = (float)Math.Sqrt(distanceSquared);
                    result.Add(linkable);
                }
            }

            return result.OrderBy(linkable => linkable.LastTestDistance).ToList();
        }

        // Todo: This will miss really long links, should we check _all_ links instead or find a smarter indexing method?
        public IList<Link> GetLinksNearPosition(Vector2 position, float distance, bool returnAfterFirst)
        {
            IList<Link> closeLinks = new List<Link>();
            IList<Particle> closeParticles = GetParticlesNearPosition(position, (float)(distance * 8));

            foreach (Particle particle in closeParticles)
            {
                foreach (Link link in particle.Links)
                {
                    if (!closeLinks.Contains(link) && link.DistanceToPoint(position) < distance)
                    {
                        closeLinks.Add(link);

                        if (returnAfterFirst)
                        {
                            return closeLinks;
                        }
                    }
                }
            }

            return closeLinks;
        }

        internal void PinJoint_Broke(object sender, EventArgs e)
        {
            Joint joint = (Joint)sender;
            Link link = (Link)joint.Tag;
            link.Delete();
        }

        protected void ClearLevel()
        {
            Task = new NeverCompleted(this);

            IList<Renderable> deleteus = Renderables.ToList();
            foreach (Renderable renderable in deleteus)
            {
                renderable.Delete();
            }

            foreach (IEngineScript script in Scripts)
            {
                script.Unprepare();
            }

            Scripts.Clear();
            PhysicsSimulator.Clear();
        }

        protected void DoUpdate()
        {
            PhysicsFrameNumber++;
            UpdateScripts();
            UpdateRenderablePhysics();
            DeleteRenderablesScheduledForDeletion();
            PhysicsSimulator.Update(TimeStep);
        }

        private void DoLevelLoaded()
        {
            if (LevelLoaded != null)
            {
                LevelLoaded(this, EventArgs.Empty);
            }
        }

        private void UpdateScripts()
        {
            foreach (IEngineScript script in Scripts)
            {
                script.UpdatePhysics();
            }
        }

        private void UpdateRenderablePhysics()
        {
            foreach (Renderable renderable in Renderables.ToList())
            {
                renderable.UpdatePhysics();
            }
        }

        private void DeleteRenderablesScheduledForDeletion()
        {
            IList<Renderable> renderablesToBeDeleted = Renderables.Where(renderable => renderable.ScheduledForDelete == true).ToList();
            foreach (Renderable renderable in renderablesToBeDeleted)
            {
                renderable.Delete();
            }
        }

        private void UpdateRenderablesGraphics()
        {
            RendererFactory.BeforeUpdate();
            _stopwatch.Reset();
            _stopwatch.Start();
            foreach (Renderable renderable in Renderables)
            {
                renderable.UpdateGraphics();
            }

            RenderFrameCount++;
            RenderTime += _stopwatch.Elapsed.TotalMilliseconds;
            RendererFactory.AfterUpdate();
        }
    }
}
